package top.lishuoboy.jdbc.a6transaction;

import lombok.extern.slf4j.Slf4j;
import top.lishuoboy.jdbc.a2conn.JDBCUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;

/**
 * <h1>6.1 数据库事务介绍</h1>
 * 1事务：一组逻辑操作单元,使数据从一种状态变换到另一种状态。
 * 2事务处理（事务操作）：保证所有事务都作为一个工作单元来执行，即使出现了故障，都不能改变这种执行方式。当在一个事务中执行多个操作时，要么所有的事务都被提交(commit)，那么这些修改就永久地保存下来；要么数据库管理系统将放弃所作的所有修改，整个事务回滚(rollback)到最初状态。
 * 3为确保数据库中数据的一致性，数据的操纵应当是离散的成组的逻辑单元：当它全部完成时，数据的一致性可以保持，而当这个单元中的一部分操作失败，整个事务应全部视为错误，所有从起始点以后的操作应全部回退到开始状态。
 * <h1>6.2 JDBC事务处理</h1>
 * 1数据一旦提交，就不可回滚。
 * 2数据什么时候意味着提交？
 * 2.1、自动提交
 * ① mysql一个连接对象被创建时，默认情况下是自动提交事务：每次执行一个 SQL 语句时，如果执行成功，就会向数据库自动提交，而不能回滚。oracle默认不是自动提交
 * ② 当前连接状态由自动提交设置 false 改为 conn.setAutoCommit(true);
 * ③ mysql5关闭数据库连接，数据就会自动的提交。如果多个操作，每个操作使用的是自己单独的连接，则无法保证事务。即同一个事务的多个操作必须在同一个连接下。mysql8和oracle相反（这才是正确的）
 * ④ DDL:操作一旦执行，都会自动提交。DML过程中不要混入DDL
 * 2.2、手动提交
 * ① DML:增删改查，主要是增删改
 * 2.3 JDBC程序中为了让多个 SQL 语句作为一个事务执行：
 * ●调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务
 * ●在所有的 SQL 语句都成功执行后，调用 commit(); 方法提交事务
 * ●在出现异常时，调用 rollback(); 方法回滚事务
 * 【注意】若此时 Connection 没有被关闭，还可能被重复使用，则需要恢复其自动提交状态setAutoCommit(true)。尤其是在使用数据库连接池技术时，执行close()方法前，建议恢复自动提交状态。
 * <h1>6.3 事务的ACID特性</h1>
 * 1. 原子性（Atomicity） 原子性是指事务是一个不可分割的工作单位，事务中的操作要么都发生，要么都不发生。
 * 2. 一致性（Consistency） 事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
 * 3. 隔离性（Isolation） 事务的隔离性是指一个事务的执行不能被其他事务干扰，即一个事务内部的操作及使用的数据对并发的其他事务是隔离的，并发执行的各个事务之间不能互相干扰。
 * 4. 持久性（Durability） 持久性是指一个事务一旦被提交，它对数据库中数据的改变就是永久性的，接下来的其他操作和数据库故障不应该对其有任何影
 *
 * @author lishuoboy
 * @date 2021-12-20 01:33
 */
@Slf4j
public class A6Transaction {
    /** 数据库类型 */
    private enum DBName {
        Mysql, Mysql8, Oracle
    }

    public static void main(String[] args) throws Exception {
        transactionAutoCommit();    // 查看、修改事务开关状态
        transactionTest();          // 事务测试
        transactionLevel();         // 查看、修改事务级别
    }

    /**
     * <h2>查看、修改事务开关状态</h2><br/>
     * mysql  默认是关闭事务的（即conn.getAutoCommit()==true） <br/>
     * oracle 默认是开启事务的（即conn.getAutoCommit()==false）
     */
    private static void transactionAutoCommit() throws Exception {
        Connection conn = JDBCUtil.getConn();
        log.info("默认是否自动提交:{}", conn.getAutoCommit());
        conn.setAutoCommit(false);
        log.info("默认是否自动提交:{}", conn.getAutoCommit());
        JDBCUtil.close(conn);
    }

    /** 事务测试 */
    private static void transactionTest() {
        Connection conn = null;
        PreparedStatement ps;
        try {
            conn = JDBCUtil.getConn();
            conn.setAutoCommit(false);

            ps = conn.prepareStatement("update bank_accout set money=money-1 where id = 1");
            ps.executeUpdate();

            System.out.println(1 / 0); // 造异常

            ps = conn.prepareStatement("update bank_accout set money=money+1 where id = 2");
            ps.executeUpdate();

            conn.commit();
            /**
             * 宋红康建议恢复默认状态。以后用数据库连接池，调用close()后并没有真正的关闭，而是放回连接池，防止其他人拿到以为是自动提交。
             * 这一行不能写到finally中，因为上面的说明 ② 当前连接状态由自动提交设置 false 改为 conn.setAutoCommit(true)，导致部分提交
             * 但是呢异常了，也就无法恢复默认状态了，解决办法是，其它人拿到连接，都显示调用conn.setAutoCommit(true)，改为默认状态
             */
            conn.setAutoCommit(true);
        } catch (Exception e) {
//            conn.rollback();
            log.error("异常", e);
        } finally {
            JDBCUtil.close(conn);
        }
    }

    /**
     * <h2>查看、修改数据库连接的事务隔离级别（不同数据库连接）</h2>
     * <br/> <h3>1、数据库并发存在的问题</h3>
     * <br/> 开启事务的A：A.setAutoCommit（false）
     * <br/> 1、脏读：     开启事务的A读取了事务B未提交的数据(增删改)。B回滚操作(手工回滚、断电、断网、连接被管理员中断等原因)
     * <br/> 2、不可重复读：开启事务的A多次select满足where条件的n条记录发现数据更新了（A未提交）。可重复读实现：简单X锁行，复杂MVVC不用锁
     * <br/> 3、幻读/虚读：开启事务的A多次select满足where条件的n条记录发现多了/少了条记录（A未提交）。解决幻读：锁表
     * <br/> 小结：不可重复读的和幻读很容易混淆，不可重复读侧重于修改，幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行，解决幻读需要锁表
     * <br/><h3>2、四个事务隔离级别</h3>
     * <br/> num  |     name         | 脏读 | 不可重复读 |  幻读
     * <br/>  1   | READ_UNCOMMITTED |  √  |    √     |   √
     * <br/>  2   | READ_COMMITTED   |  ×  |    √     |   √
     * <br/>  4   | REPEATABLE_READ  |  ×  |    ×     |   √
     * <br/>  8   | SERIALIZABLE     |  ×  |    ×     |   ×
     * <br/> <h3>Mysql 4种隔离级别（实现原理是通过【版本号|时间戳】 + 【提交标识】  实现），oracle只支持②④：</h3>
     * <br/> ① 1：(读未提交) READ-UNCOMMITTED： 最低级别，任何情况都无法保证。（原理：where条件相同的读取提交标识为未提交的）
     * <br/> ② 2：(读已提交) READ-COMMITTED：   可避免脏读 的发生。<b>Oracle默认级别（大部分数据库也是） </b>。（原理：where条件相同的读取提交标识为已提交的）
     * <br/> ③ 4：(可重复读) REPEATABLE-READ：  可避免脏读、不可重复读 的发生。<b>Mysql默认级别 </b>（原理：select时记住对应版本号，再次查询时还查此版本的）
     * <br/> ④ 8：(串行化)   SERIALIZABLE：    可避免脏读、不可重复读、幻读 的发生。（原理：select时锁住where条件，其他事务不能增、删、改此where条件的）
     */
    private static void transactionLevel() {
        Connection conn = null;
        try {
            conn = JDBCUtil.getConn();
            // 打印当前默认事务级别
            log.info("当前默认事务级别:level=={}, name={}", conn.getTransactionIsolation(), getTransactionIsolationLevel(conn, DBName.Mysql8));
            // 修改事务级别并打印，只对当前连接有效。如需修改默认事务级别，去该数据库默认实务级别或修改数据库引擎
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
            log.info("level=={}, name={}", conn.getTransactionIsolation(), getTransactionIsolationLevel(conn, DBName.Mysql8));
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            log.info("level=={}, name={}", conn.getTransactionIsolation(), getTransactionIsolationLevel(conn, DBName.Mysql8));
            conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
            log.info("level=={}, name={}", conn.getTransactionIsolation(), getTransactionIsolationLevel(conn, DBName.Mysql8));
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            log.info("level=={}, name={}", conn.getTransactionIsolation(), getTransactionIsolationLevel(conn, DBName.Mysql8));
        } catch (Exception e) {
            log.error("异常", e);
        } finally {
            JDBCUtil.close(conn);
        }
    }

    /**
     * <h2>查看数据库事务级别<h2>
     *
     * @return 数据库事务级别
     */
    private static String getTransactionIsolationLevel(Connection conn, DBName dbName) {
        String txLevel = "";
        Statement st = null;
        try {
            st = conn.createStatement();
            String sql = null;
            if (dbName.equals(DBName.Mysql)) {
                sql = "select @@tx_isolation";
            } else if (dbName.equals(DBName.Mysql8)) {
                sql = "select @@transaction_isolation";
            } else if (dbName.equals(DBName.Oracle)) {
                txLevel = "暂不支持获取数据库名称的数据库,dbName=" + dbName;
                return txLevel;
            }
            ResultSet rs = st.executeQuery(sql);
            while (rs.next()) {
                txLevel = rs.getString(1);
            }
            JDBCUtil.close(rs);
            return txLevel;
        } catch (Exception e) {
            log.error("异常", e);
            return e.getMessage();
        } finally {
            JDBCUtil.close(st);
        }
    }
}

